using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Input.Manipulations;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Threading;

namespace Wintellect.Touch
{
    /// <summary>
    /// <see cref="Behavior"/> that provides Manipulation event notifications when touches occur on an associated <see cref="UIElement"/>
    /// </summary>
    public class Manipulate2DBehavior : Behavior<UIElement>
    {
        #region Fields
        private const Double MOUSE_MINIMUM_OFFSET = 5.0;

        private const float DefaultDpi = 96.0f;

        // deceleration: inches/second squared (remember - the higher the numerator, the quicker it slows down)
        private const float DefaultDeceleration = 15.0f * DefaultDpi / (1000.0f * 1000.0f);

        /// <summary>
        /// The manipulation processing engine
        /// </summary>
        private readonly ManipulationProcessor2D _manipulationProcessor;

        /// <summary>
        /// The inertia processing engine (works with <see cref="_manipulationProcessor"/>)
        /// </summary>
        private readonly InertiaProcessor2D _inertiaProcessor;

        /// <summary>
        /// Timer to use for determining if inertia should be kicked off...
        /// </summary>
        private readonly DispatcherTimer _inertiaTimer;

        /// <summary>
        /// All subscribed behaviors.
        /// </summary>
        private static readonly List<Manipulate2DBehavior> _subscribedBehaviors = new List<Manipulate2DBehavior>();

        /// <summary>
        /// Behaviors that have been "captured" by a specific touch point (touchpointId, behavior)
        /// </summary>
        private static readonly Dictionary<Manipulate2DBehavior, List<Int32>> _capturedBehaviors = new Dictionary<Manipulate2DBehavior, List<Int32>>();

        /// <summary>
        /// Flag to indicate if touch events have been statically hooked or not.
        /// </summary>
        private static Boolean _touchEventsHooked;

        #endregion

        #region Behavior Overrides

        /// <summary>
        /// Called after the behavior is attached to an AssociatedObject.
        /// </summary>
        /// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
        protected override void OnAttached()
        {
            base.OnAttached();
            HookTouchEvents();
            HookMouseEvents();
            _subscribedBehaviors.Add(this);
        }

        /// <summary>
        /// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
        /// </summary>
        /// <remarks>Override this to unhook functionality from the AssociatedObject.</remarks>
        protected override void OnDetaching()
        {
            base.OnDetaching();
            UnhookMouseEvents();
            _subscribedBehaviors.Remove(this);
        }

        #endregion

        #region Constructor(s) and Initialization

        /// <summary>
        /// Initializes a new instance of the <see cref="Manipulate2DBehavior "/> class.
        /// </summary>
        public Manipulate2DBehavior()
        {
            //_manipulationProcessor = new ManipulationProcessor2D(Manipulations2D.Translate);
            _manipulationProcessor = new ManipulationProcessor2D(Manipulations2D.All);
            _manipulationProcessor.Started += (o, e) => ManipulationStarted(AssociatedObject, e);
            _manipulationProcessor.Delta += (o, e) =>
                                                {
                                                    //if (e.Cumulative.ExpansionX != 0 || e.Cumulative.ExpansionY != 0 || e.Cumulative.ScaleX != 1 || e.Cumulative.ScaleY != 1)
                                                    //{
                                                    //    System.Diagnostics.Debug.WriteLine("ExpansionX" + e.Cumulative.ExpansionX);
                                                    //    System.Diagnostics.Debug.WriteLine("ExpansionY" + e.Cumulative.ExpansionY);
                                                    //    System.Diagnostics.Debug.WriteLine("ScaleX" + e.Cumulative.ScaleX);
                                                    //    System.Diagnostics.Debug.WriteLine("ScaleY" + e.Cumulative.ScaleY);
                                                    //}
                                                    ManipulationDelta(AssociatedObject, e);
                                                };
            _manipulationProcessor.Completed += OnManipulationCompleted;


            _inertiaProcessor = new InertiaProcessor2D
                                    {
                                        //TranslationBehavior = {DesiredDeceleration = 0.002f}
                                        TranslationBehavior = { DesiredDeceleration = DefaultDeceleration }
                                    };
            _inertiaProcessor.Delta += OnInertiaDelta;
            _inertiaProcessor.Completed += OnInertiaCompleted;
            InertiaDeceleration = 10.0f*DefaultDpi/(1000.0f*1000.0f);

            _inertiaTimer = new DispatcherTimer {Interval = TimeSpan.FromMilliseconds(3)};
            _inertiaTimer.Tick += OnInertiaTimerTick;
        }

        #endregion

        public Boolean IsInertial { get; set; }


        /// <summary>
        /// Gets or sets the inertia deceleration in pixels per second per second.
        /// </summary>
        /// <value>The inertia deceleration.</value>
        public Double InertiaDeceleration { get; set; }

        #region Primitive Touch Events

        private static void HookTouchEvents()
        {
            if (!_touchEventsHooked)
            {
                _touchEventsHooked = true;
                System.Windows.Input.Touch.FrameReported += OnFrameReported;
            }
        }

        /// <summary>
        /// Called when a touch frame event has occurred.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.TouchFrameEventArgs"/> instance containing the event data.</param>
        private static void OnFrameReported(Object sender, TouchFrameEventArgs e)
        {
            TouchPointCollection touchPoints = e.GetTouchPoints(Application.Current.RootVisual);
            ProcessTouchPoints(touchPoints.Select(x => TouchEventData.FromTouchPoint(x)));
        }

        private static void ProcessTouchPoints(IEnumerable<TouchEventData> touchPoints)
        {
            // Find any new touch-point/behavior associations.
            foreach (var touchPoint in touchPoints.Where(x => x.Action == TouchAction.Down))
            {
                // There shouldn't be any of these!
                if (!_capturedBehaviors.Any(x => x.Value.Contains(touchPoint.DeviceId)))
                {
                    // "Un-owned" touch device - see if some behaviors can be found to be associated with it at its current position...
                    var matchingBehaviors = GetBehaviorAssociatedWithTouchPoint(touchPoint.Position, _subscribedBehaviors);
                    if (matchingBehaviors != null)
                    {
                        foreach(var behavior in matchingBehaviors)
                        {
                            if (_capturedBehaviors.ContainsKey(behavior))
                            {
                                var behaviorDeviceIds = _capturedBehaviors[behavior];
                                if (!behaviorDeviceIds.Contains(touchPoint.DeviceId))
                                {
                                    behaviorDeviceIds.Add(touchPoint.DeviceId);
                                }
                            }
                            else
                            {
                                _capturedBehaviors.Add(behavior, new List<Int32>{touchPoint.DeviceId});
                            }
                        }
                    }
                }
            }

            // Process any current touch-point/behaviors
            foreach (var capturedBehaviorItem in _capturedBehaviors)
            {
                var associatedTouchPoints = touchPoints.Where(x => capturedBehaviorItem.Value.Contains(x.DeviceId)).ToList();
                capturedBehaviorItem.Key.HandleTouchEvents(associatedTouchPoints);
            }

            // Find and remove touch point/behavior associations as needed
            foreach (var touchPoint in touchPoints.Where(x => x.Action == TouchAction.Up))
            {
                var affectedBehaviors = _capturedBehaviors.Where(x => x.Value.Contains(touchPoint.DeviceId)).ToList();
                foreach (var affectedBehavior in affectedBehaviors)
                {
                    affectedBehavior.Value.Remove(touchPoint.DeviceId);
                    if (!affectedBehavior.Value.Any())
                    {
                        _capturedBehaviors.Remove(affectedBehavior.Key);
                        affectedBehavior.Key.CompleteManipulation();
                    }
                }
            }
        }

        /// <summary>
        /// Gets the behavior associated with a touch point.
        /// </summary>
        /// <param name="touchPointPosition">The touch point position.</param>
        /// <param name="behaviorsToCheck">The behaviors to check.</param>
        /// <returns></returns>
        /// <remarks>
        /// Retrieves the list of elements underneath the given touch point position, and looks for the first one that has an associated <see cref="Manipulate2DBehavior"/>
        /// </remarks>
        private static IEnumerable<Manipulate2DBehavior> GetBehaviorAssociatedWithTouchPoint(Point touchPointPosition, IEnumerable<Manipulate2DBehavior> behaviorsToCheck)
        {
            var matchedBehaviors = new List<Manipulate2DBehavior>();
            var allPopups = VisualTreeHelper.GetOpenPopups();
            var allChildWindows = allPopups.Where(x => x.Child is ChildWindow);
            var topmostChildWindow = allChildWindows.FirstOrDefault();

            IEnumerable<UIElement> elements;
            if (topmostChildWindow != null)
            {
                elements = VisualTreeHelper.FindElementsInHostCoordinates(touchPointPosition, topmostChildWindow);
            }
            else
            {
                elements = VisualTreeHelper.FindElementsInHostCoordinates(touchPointPosition, Application.Current.RootVisual);
            }

            foreach (var element in elements)
            {
                var matchedBehavior = behaviorsToCheck.FirstOrDefault(x => x.AssociatedObject == element);
                if (matchedBehavior != null) matchedBehaviors.Add(matchedBehavior);
            }
            return matchedBehaviors;
        }

        #endregion

        #region Mouse Events

        private void HookMouseEvents()
        {
            AssociatedObject.MouseLeftButtonDown += HandleMouseLeftButtonDown;
            AssociatedObject.MouseMove += HandleMouseMove;
            AssociatedObject.MouseLeftButtonUp += HandleMouseLeftButtonUp;
        }

        private void UnhookMouseEvents()
        {
            AssociatedObject.MouseLeftButtonDown -= HandleMouseLeftButtonDown;
            AssociatedObject.MouseMove -= HandleMouseMove;
            AssociatedObject.MouseLeftButtonUp -= HandleMouseLeftButtonUp;
        }

        private Boolean _isMouseCaptured;

        private void HandleMouseLeftButtonDown(Object sender, MouseButtonEventArgs e)
        {
            // Ignore stylus/touch input
            if (e.StylusDevice.DeviceType != TabletDeviceType.Mouse) return;

            AssociatedObject.CaptureMouse();
            _isMouseCaptured = true;

            var pos = e.GetPosition(Application.Current.RootVisual);
            var touchData = new TouchEventData
                                 {
                                     DeviceId = -1,
                                     Position = pos,
                                     Action = TouchAction.Down,
                                 };
            ProcessTouchPoints(new[] { touchData });
        }

        private void HandleMouseMove(Object sender, MouseEventArgs e)
        {
            if (!_isMouseCaptured) return;
            // Ignore stylus/touch input
            if (e.StylusDevice.DeviceType != TabletDeviceType.Mouse) return;

            var pos = e.GetPosition(Application.Current.RootVisual);
            var touchData = new TouchEventData
            {
                DeviceId = -1,
                Position = pos,
                Action = TouchAction.Move,
            };
            ProcessTouchPoints(new[] { touchData });
        }

        private void HandleMouseLeftButtonUp(Object sender, MouseButtonEventArgs e)
        {
            if (!_isMouseCaptured) return;
            // Ignore stylus/touch input
            if (e.StylusDevice.DeviceType != TabletDeviceType.Mouse) return;

            var pos = e.GetPosition(Application.Current.RootVisual);
            var touchData = new TouchEventData
            {
                DeviceId = -1,
                Position = pos,
                Action = TouchAction.Up,
            };
            ProcessTouchPoints(new[] { touchData });

            _isMouseCaptured = false;
            AssociatedObject.ReleaseMouseCapture();
        }

        #endregion

        #region Touch-to-Manipulation Bridging Methods
        private class TouchEventData
        {
            public static TouchEventData FromTouchPoint(TouchPoint touchPoint)
            {
                var touchEventData = new TouchEventData
                                         {
                                             DeviceId = touchPoint.TouchDevice.Id,
                                             PositionX = (float)touchPoint.Position.X,
                                             PositionY = (float)touchPoint.Position.Y,
                                             Action = touchPoint.Action,
                                         };
                return touchEventData;
            }

            public Int32 DeviceId { get; set; }
            public float PositionX { get; set; }
            public float PositionY { get; set; }
            public TouchAction Action { get; set; }
            public Point Position
            {
                get { return new Point(PositionX, PositionY);}
                set { 
                    PositionX = (float)value.X;
                    PositionY = (float)value.Y;
                }
            }
        }

        private void HandleTouchEvents(IEnumerable<TouchEventData> points)
        {
            StopInertia();

            IEnumerable<Manipulator2D> manips = points.Select(p => new Manipulator2D
                                                                       {
                                                                           Id = p.DeviceId,
                                                                           X = p.PositionX,
                                                                           Y = p.PositionY
                                                                       });

            _manipulationProcessor.ProcessManipulators(DateTime.Now.Ticks, manips);
        }

        private void CompleteManipulation()
        {
            _manipulationProcessor.CompleteManipulation(DateTime.Now.Ticks);
        }
        #endregion

        #region Manipulation Events & Handlers

        public event EventHandler<Manipulation2DStartedEventArgs> ManipulationStarted = delegate { };
        public event EventHandler<Manipulation2DDeltaEventArgs> ManipulationDelta = delegate { };
        public event EventHandler<Manipulation2DCompletedEventArgs> ManipulationCompleted = delegate { };
 
        private void OnManipulationCompleted(Object sender, Manipulation2DCompletedEventArgs e)
        {
            if (IsInertial)
            {
                // Trigger inertia behavior
                if (Math.Pow(e.Total.TranslationX, 2) + Math.Pow(e.Total.TranslationY, 2) >= Math.Pow(MOUSE_MINIMUM_OFFSET, 2))
                {
                    StartInertia(new Point(e.Velocities.LinearVelocityX, e.Velocities.LinearVelocityY));
                }
                    
            }

            // Echo the event
            ManipulationCompleted(AssociatedObject, e);
        }

        #endregion

        #region Inertia Processing

        private void StartInertia(Point linearVelocity)
        {
            _inertiaProcessor.TranslationBehavior.InitialVelocityX = (float)linearVelocity.X;// / 5.0f;
            _inertiaProcessor.TranslationBehavior.InitialVelocityY = (float)linearVelocity.Y;// / 5.0f;
            _inertiaTimer.Start();
        }

        private void OnInertiaDelta(Object sender, Manipulation2DDeltaEventArgs args)
        {
            ManipulationDelta(AssociatedObject, args);
        }

        private void OnInertiaCompleted(Object sender, Manipulation2DCompletedEventArgs e)
        {
            _inertiaTimer.Stop();
        }

        private void OnInertiaTimerTick(Object sender, EventArgs e)
        {
            _inertiaProcessor.Process(DateTime.Now.Ticks);
        }

        private void StopInertia()
        {
            if (_inertiaTimer.IsEnabled)
            {
                _inertiaTimer.Stop();
                _inertiaProcessor.Complete(DateTime.Now.Ticks);
            }
        }

        #endregion
    }
}